﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Diagnostics;
using System.IO;
using System.Xml;
using System.Drawing.Imaging;
using System.Diagnostics.CodeAnalysis;

namespace AndersLiu.Bambook.Snb
{
    /// <summary>
    /// 所有与snb文件相关类型的基类。
    /// 该类还将验证SDK的可访问性和版本。
    /// </summary>
    public abstract class SnbOjbect
    {
        [SuppressMessage("Microsoft.Design", "CA1065")]
        static SnbOjbect()
        {
            //
            // 验证SDK的可访问性和版本。
            //
            int version = 0;
            try
            {
                var r = BambookSdk.GetSDKVersion(out version);
                if (r != BambookSdk.Result.Success)
                    throw new BambookSdkResultException(r);
            }
            catch (Exception ex)
            {
                throw new BambookSdkNotReadyException(ex);
            }

            if (version != BambookSdk.VERSION)
                throw new BambookSdkInvalidVersionException(version);
        }
    }

    /// <summary>
    /// 表示一个内存中的snb文件。
    /// </summary>
    /// <remarks>
    /// 该类不是线程安全的，请不要在加载或保存过程中对其进行操作。
    /// </remarks>
    public class SnbFile : SnbOjbect
    {
        public SnbFile()
        {
            this.Version = new Version(1, 0);
            this.Language = "zh-CN";
            this.Chapters = new List<SnbChapter>();
        }

        /// <summary>
        /// 获取snb文件的版本。
        /// </summary>
        /// <remarks>
        /// 目前只能是1.0。
        /// </remarks>
        public Version Version { get; private set; }

        /// <summary>
        /// 获取或设置书名。
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// 获取或设置书籍的作者。
        /// </summary>
        public string Author { get; set; }

        /// <summary>
        /// 获取书籍的语言。
        /// </summary>
        /// <remarks>
        /// 目前只能是“zh-CN”。
        /// </remarks>
        public string Language { get; private set; }

        /// <summary>
        /// 获取或设置书籍的版权信息。
        /// </summary>
        public string Copyright { get; set; }

        /// <summary>
        /// 获取或设置书籍的出版社。
        /// </summary>
        public string Publisher { get; set; }

        /// <summary>
        /// 获取或设置生成工具的信息。
        /// </summary>
        public string Generator { get; set; }

        /// <summary>
        /// 获取创建时间。
        /// </summary>
        /// <remarks>
        /// 在保存时，会自动设置为当前本地时间。
        /// </remarks>
        public DateTime CreatedTime { get; set; }

        /// <summary>
        /// 获取或设置书籍的摘要。
        /// </summary>
        public string Abstract { get; set; }

        /// <summary>
        /// 获取或设置书籍的封面。
        /// </summary>
        public Bitmap Cover { get; set; }

        /// <summary>
        /// 获取书籍的章节列表。
        /// </summary>
        public List<SnbChapter> Chapters { get; private set; }

        /// <summary>
        /// 将给定的snb文件加载到当前对象。
        /// </summary>
        /// <remarks>
        /// 该操作将覆盖现有对象，即对现有对象进行的更改将被丢弃。
        /// </remarks>
        /// <param name="snbFilePath">snb文件的路径。</param>
        public void Load(string snbFilePath)
        {
        }

        /// <summary>
        /// 将当前对象保存到给定的snb文件中。
        /// </summary>
        /// <remarks>
        /// 该方法会将<see cref="CreatedTime"/>属性重写为当前时间。
        /// </remarks>
        /// <param name="snbFilePath">snb文件的路径。</param>
        public void Save(string snbFilePath, SnbProgressChangedCallback cb)
        {
            var rootDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());

            try
            {
                SavingContext.Save(this, rootDir, snbFilePath, cb);
            }
            finally
            {
                Directory.Delete(rootDir, true);
            }
        }

        private class SavingContext
        {
            internal static void Save(SnbFile snbFile,
                string rootDir, string snbFilePath,
                SnbProgressChangedCallback cb)
            {
                Debug.Assert(snbFile != null);
                Debug.Assert(rootDir != null);
                Debug.Assert(snbFilePath != null);

                var context = new SavingContext(snbFile, rootDir, snbFilePath);
                context.Initialize(cb);
                context.SaveChapters(cb);
                context.SaveImages(cb);
                context.SaveTocSnbf(cb);
                context.SaveBookSnbf(cb);
                context.CreateSnbFile(cb);
                context.VerifySnbFile(cb);
            }

            private SavingContext(SnbFile snbFile, string rootDir, string snbFilePath)
            {
                _snbFile = snbFile;
                _rootDir = rootDir;
                _snbFilePath = snbFilePath;
            }

            private SnbFile _snbFile;
            private string _rootDir;
            private string _snbFilePath;

            private string _snbfDir;
            private string _snbcDir;
            private string _snbcImagesDir;
            private Dictionary<Image, int> _imageIdMap;

            private void Initialize(SnbProgressChangedCallback cb)
            {
                if (cb != null)
                    cb(SnbProgress.Initializing, 0, 0, 0);

                CreateDirectories();
                CreateImageIdMap();
            }

            private void CreateDirectories()
            {
                _snbfDir = Path.Combine(_rootDir, "snbf");
                _snbcDir = Path.Combine(_rootDir, "snbc");
                _snbcImagesDir = Path.Combine(_snbcDir, "images");
                Directory.CreateDirectory(_snbfDir);
                Directory.CreateDirectory(_snbcDir);
                Directory.CreateDirectory(_snbcImagesDir);
                new FileInfo(_snbFilePath).Directory.Create();
            }

            private void CreateImageIdMap()
            {
                var imageIdMap = new Dictionary<Image, int>();
                var nImage = 1;
                if (_snbFile.Cover != null)
                    imageIdMap.Add(_snbFile.Cover, nImage++);

                foreach (var ch in _snbFile.Chapters)
                {
                    foreach (var para in ch.Paragraphs)
                    {
                        var imgPara = para as SnbImageParagraph;

                        if (imgPara != null && !imageIdMap.ContainsKey(imgPara.Image))
                            imageIdMap.Add(imgPara.Image, nImage++);
                    }
                }

                _imageIdMap = imageIdMap;
            }

            private string GetChapterPath(int iCharpter)
            {
                return string.Format("c{0}.snbc", iCharpter + 1);
            }

            private string GetImagePath(Image image)
            {
                return ConstructImagePath(_imageIdMap.Count, _imageIdMap[image]);
            }

            private string ConstructImagePath(int totalNumber, int currentNumber)
            {
                const int DIR_LEN = 2;
                const string SUFFIX = ".png";

                var strMaxId = totalNumber.ToString();
                var depth = (strMaxId.Length + DIR_LEN - 1) / DIR_LEN;
                var chars = new char[strMaxId.Length + depth - 1 + SUFFIX.Length];
                for (var i = chars.Length - 1; i >= chars.Length - SUFFIX.Length; i--)
                    chars[i] = SUFFIX[SUFFIX.Length - (chars.Length - i)];

                var strId = currentNumber.ToString();
                var iId = strId.Length - 1;
                var nPart = 0;
                for (var iChar = chars.Length - SUFFIX.Length - 1; iChar >= 0; iChar--)
                {
                    if (nPart >= DIR_LEN)
                    {
                        chars[iChar] = '\\';
                        nPart = 0;
                    }
                    else
                    {
                        chars[iChar] = iId >= 0 ? strId[iId] : '0';
                        iId--;
                        nPart++;
                    }
                }

                return new string(chars);
            }

            [SuppressMessage("Microsoft.Usage", "CA2202")]
            private void SaveBookSnbf(SnbProgressChangedCallback cb)
            {
                if (cb != null)
                    cb(SnbProgress.SavingBookSnbf, 0, 0, 0);

                var path = Path.Combine(_snbfDir, @"book.snbf");

                using (var stream = File.Create(path))
                {
                    using (var writer = new XmlTextWriter(stream, Encoding.UTF8))
                    {
                        _snbFile.CreatedTime = DateTime.Now;

                        writer.WriteStartDocument();

                        writer.WriteStartElement("book-snbf");
                        // TODO: check invalid version
                        writer.WriteAttributeString("version", _snbFile.Version.ToString(2));

                        writer.WriteStartElement("head");
                        writer.WriteElementString("name", _snbFile.Name);
                        writer.WriteElementString("author", _snbFile.Author);
                        // TODO: check invalid language
                        writer.WriteElementString("language", _snbFile.Language);
                        writer.WriteElementString("rights", _snbFile.Copyright);
                        writer.WriteElementString("publisher", _snbFile.Publisher);
                        writer.WriteElementString("generator", _snbFile.Generator);
                        writer.WriteElementString("created", _snbFile.CreatedTime.ToString("yyyy-MM-dd hh:mm"));
                        writer.WriteElementString("abstract", _snbFile.Abstract);
                        writer.WriteElementString("cover", _snbFile.Cover == null ? null : "cover.png");
                        writer.WriteEndElement();  // head

                        writer.WriteEndElement();  // book-snbf
                        writer.WriteEndDocument();
                    }
                }
            }

            [SuppressMessage("Microsoft.Usage", "CA2202")]
            private void SaveTocSnbf(SnbProgressChangedCallback cb)
            {
                if (cb != null)
                    cb(SnbProgress.SavingTocSnbf, 0, 0, 0);

                var path = Path.Combine(_snbfDir, @"toc.snbf");

                using (var stream = File.Create(path))
                {
                    using (var writer = new XmlTextWriter(stream, Encoding.UTF8))
                    {
                        writer.WriteStartDocument();
                        writer.WriteStartElement("toc-snbf");

                        writer.WriteStartElement("head");
                        writer.WriteElementString("chapters", _snbFile.Chapters.Count.ToString());
                        writer.WriteEndElement();  // head

                        writer.WriteStartElement("body");
                        for (var i = 1; i <= _snbFile.Chapters.Count; i++)
                        {
                            writer.WriteStartElement("chapter");
                            writer.WriteAttributeString("src", string.Format("c{0}.snbc", i));
                            writer.WriteString(_snbFile.Chapters[i - 1].Title);
                            writer.WriteEndElement();  // chapter
                        }
                        writer.WriteEndElement();  // body

                        writer.WriteEndElement();  // toc-snbf
                        writer.WriteEndDocument();
                    }
                }
            }

            private void SaveChapters(SnbProgressChangedCallback cb)
            {
                for (var i = 0; i < _snbFile.Chapters.Count; i++)
                {
                    if (cb != null)
                        cb(SnbProgress.SavingChapters, i + 1, 0, 0);

                    SaveChapter(i);
                }
            }

            [SuppressMessage("Microsoft.Usage", "CA2202")]
            private void SaveChapter(int iChapter)
            {
                var path = Path.Combine(_snbcDir, GetChapterPath(iChapter));

                using (var stream = File.Create(path))
                {
                    using (var writer = new XmlTextWriter(stream, Encoding.UTF8))
                    {
                        var chapter = _snbFile.Chapters[iChapter];

                        writer.WriteStartDocument();

                        writer.WriteStartElement("snbc");

                        writer.WriteStartElement("head");
                        writer.WriteElementString("title", chapter.Title);
                        writer.WriteEndElement();  // head

                        writer.WriteStartElement("body");
                        for (var c = 0; c < chapter.Paragraphs.Count; c++)
                        {
                            SnbParagraph p = chapter.Paragraphs[c];
                            if (p is SnbTextParagraph)
                            {
                                WriteTextParagraph(writer, (SnbTextParagraph)p);
                            }
                            else if (p is SnbImageParagraph)
                            {
                                WriteImageParagraph(writer, (SnbImageParagraph)p);
                            }
                            else
                                throw new NotSupportedException();
                        }
                        writer.WriteEndElement();  // body

                        writer.WriteEndElement();  // snbc
                        writer.WriteEndDocument();
                    }
                }
            }

            private void WriteTextParagraph(XmlWriter writer, SnbTextParagraph para)
            {
                writer.WriteStartElement("text");
                writer.WriteString(para.Text);
                writer.WriteEndElement();  // text
            }

            private void WriteImageParagraph(XmlWriter writer, SnbImageParagraph para)
            {
                writer.WriteStartElement("img");
                if (para.Width > 0 && para.Height > 0)
                {
                    writer.WriteAttributeString("width", para.Width.ToString());
                    writer.WriteAttributeString("height", para.Height.ToString());
                }
                writer.WriteString(GetImagePath(para.Image));
                writer.WriteEndElement();  // img
            }

            private void SaveImages(SnbProgressChangedCallback cb)
            {
                var nImage = 1;
                var nTotalImages = _imageIdMap.Count;

                foreach (var image in _imageIdMap.Keys)
                {
                    if (cb != null)
                        cb(SnbProgress.SavingImages, 0, nImage++, nTotalImages);

                    var path = Path.Combine(_snbcImagesDir, GetImagePath(image));
                    new FileInfo(path).Directory.Create();
                    image.Save(path, ImageFormat.Png);
                }
            }

            private void CreateSnbFile(SnbProgressChangedCallback cb)
            {
                if (cb != null)
                    cb(SnbProgress.Packing, 0, 0, 0);

                var r = BambookSdk.PackSnbFromDirectory(_snbFilePath, _rootDir);
                if (r != BambookSdk.Result.Success)
                    throw new BambookSdkResultException(r);
            }

            private void VerifySnbFile(SnbProgressChangedCallback cb)
            {
                if (cb != null)
                    cb(SnbProgress.Verifying, 0, 0, 0);

                var r = BambookSdk.VerifySnbFile(_snbFilePath);
                if (r != BambookSdk.Result.Success)
                    throw new BambookSdkResultException(r);
            }
        }
    }

    /// <summary>
    /// 表示正在对snb文件进行的操作。
    /// </summary>
    public enum SnbProgress
    {
        /// <summary>
        /// 正在初始化snb文件。
        /// </summary>
        Initializing = 0,

        /// <summary>
        /// 正在保存book.snbf文件。
        /// </summary>
        SavingBookSnbf = 101,

        /// <summary>
        /// 正在保存toc.snbf文件。
        /// </summary>
        SavingTocSnbf = 102,

        /// <summary>
        /// 正在保存每一章的snbc文件。
        /// </summary>
        SavingChapters = 103,

        /// <summary>
        /// 正在保存图片文件。
        /// </summary>
        SavingImages = 104,

        /// <summary>
        /// 正在打包snb文件。
        /// </summary>
        Packing = 105,

        /// <summary>
        /// 正在验证snb文件。
        /// </summary>
        Verifying = 106,
    }

    /// <summary>
    /// 用于报告snb文件处理进度的回调委托。
    /// </summary>
    /// <param name="action">正在进行的操作。</param>
    /// <param name="component">正在操作的snb组成部分。</param>
    /// <param name="nChapter">正在操作的章节编号，从1开始计算。
    /// 如果当前操作与章节无关，则为0。</param>
    /// <param name="nParagraph">正在操作的段落编号，从1开始计算。
    /// 如果当前操作与段落无关，则为0。</param>
    /// <param name="nImage">正在操作的图片编号，从1开始计算。
    /// 如果当前操作与图片无关，则为0。</param>
    /// <param name="nTotalImages">图片总数。</param>
    public delegate void SnbProgressChangedCallback(SnbProgress progress,
        int nChapter, int nImage, int nTotalImages);

    /// <summary>
    /// 表示snb文件中的一个章节。
    /// </summary>
    public class SnbChapter : SnbOjbect
    {
        public SnbChapter(string title)
        {
            this.Title = title;
            this.Paragraphs = new List<SnbParagraph>();
        }

        /// <summary>
        /// 获取或设置章节标题。
        /// </summary>
        public string Title
        {
            get { return _title; }
            set
            {
                if (value == null)
                    throw new ArgumentNullException("value");

                _title = value;
            }
        }
        private string _title;

        /// <summary>
        /// 获取章节中的段落列表。
        /// </summary>
        public List<SnbParagraph> Paragraphs { get; private set; }
    }

    /// <summary>
    /// 表示一个段落。
    /// </summary>
    public abstract class SnbParagraph : SnbOjbect
    {
    }

    /// <summary>
    /// 表示一个纯本文段落。
    /// </summary>
    public class SnbTextParagraph : SnbParagraph
    {
        public SnbTextParagraph(string text)
        {
            this.Text = text;
        }

        /// <summary>
        /// 获取或设置段落的内容。
        /// </summary>
        public string Text
        {
            get { return _text; }
            set
            {
                if (value == null)
                    throw new ArgumentNullException("value");

                _text = value;
            }
        }
        private string _text;
    }

    /// <summary>
    /// 表示一个图片段落。
    /// </summary>
    public class SnbImageParagraph : SnbParagraph
    {
        public SnbImageParagraph(Image image)
        {
            this.Image = image;
            this.Width = image.Width;
            this.Height = image.Height;
        }

        /// <summary>
        /// 获取或设置段落中的图片。
        /// </summary>
        public Image Image
        {
            get { return _image; }
            set
            {
                if (value == null)
                    throw new ArgumentNullException("value");

                _image = value;
            }
        }
        private Image _image;

        /// <summary>
        /// 获取或设置图片的宽度，为0表示不指定宽度，由系统决定。
        /// </summary>
        public int Width
        {
            get { return _width; }
            set
            {
                if (value < 0)
                    throw new ArgumentOutOfRangeException("value");

                _width = value;
            }
        }
        private int _width;

        /// <summary>
        /// 获取或设置图片的高度，为0表示不指定高度，由系统决定。
        /// </summary>
        public int Height
        {
            get { return _height; }
            set
            {
                if (value < 0)
                    throw new ArgumentOutOfRangeException("value");

                _height = value;
            }
        }
        private int _height;
    }

    /// <summary>
    /// 表示调用Bambook SDK API得到的结果不是Success。
    /// </summary>
    [SuppressMessage("Microsoft.Usage", "CA2237")]
    public class BambookSdkResultException : Exception
    {
        public BambookSdk.Result Result { get; private set; }

        internal BambookSdkResultException(BambookSdk.Result result)
            : base(BambookSdk.GetErrorString(result))
        {
            Debug.Assert(result != BambookSdk.Result.Success);

            this.Result = result;
        }
    }

    /// <summary>
    /// 表示无法访问Bambook SDK。
    /// </summary>
    [SuppressMessage("Microsoft.Usage", "CA2237")]
    public class BambookSdkNotReadyException : Exception
    {
        internal BambookSdkNotReadyException(Exception innerException)
            : base(null, innerException)
        {
        }
    }

    /// <summary>
    /// 表示无法支持的Bambook SDK版本。
    /// </summary>
    [SuppressMessage("Microsoft.Usage", "CA2237")]
    public class BambookSdkInvalidVersionException : Exception
    {
        public int SupportedVersion { get; private set; }
        public int RealVersion { get; private set; }

        internal BambookSdkInvalidVersionException(int realVersion)
        {
            this.SupportedVersion = BambookSdk.VERSION;
            this.RealVersion = realVersion;
        }
    }

    /// <summary>
    /// 封装Bambook SDK。
    /// </summary>
    public static class BambookSdk
    {
        internal static int VERSION = 0x00090000;

        public enum Result
        {
            Success = 0,
            Fail = 1001,
            NotImplemented = 1002,
            Disconnected = 1003,
            ParameterError = 1004,
            Timeout = 1005,
            InvalidHandle = 1006,
            InvalidFile = 1007,
            InvalidDirectory = 1008,
            Busy = 1010,
            EndOfFile = 1011,
            IOError = 1012,
            FileNotInside = 1013,
        }

        [SuppressMessage("Microsoft.Design", "CA1060")]
        [DllImport("BambookCore.dll",
            EntryPoint = "BambookGetSDKVersion",
            CallingConvention = CallingConvention.Cdecl,
            CharSet = CharSet.Ansi,
            ExactSpelling = true,
            PreserveSig = true)]
        internal static extern Result GetSDKVersion(
            [MarshalAs(UnmanagedType.U4), Out] out int version);

        [SuppressMessage("Microsoft.Design", "CA1060")]
        [DllImport("BambookCore.dll",
            EntryPoint = "BambookPackSnbFromDir",
            CallingConvention = CallingConvention.Cdecl,
            CharSet = CharSet.Ansi,
            BestFitMapping = false,
            ThrowOnUnmappableChar = true,
            ExactSpelling = true,
            PreserveSig = true)]
        internal static extern Result PackSnbFromDirectory(
            [MarshalAs(UnmanagedType.LPStr), In] string snbName,
            [MarshalAs(UnmanagedType.LPStr), In] string rootDir);

        [SuppressMessage("Microsoft.Design", "CA1060")]
        [DllImport("BambookCore.dll",
            EntryPoint = "BambookUnpackFileFromSnb",
            CallingConvention = CallingConvention.Cdecl,
            CharSet = CharSet.Ansi,
            BestFitMapping = false,
            ThrowOnUnmappableChar = true,
            ExactSpelling = true,
            PreserveSig = true)]
        internal static extern Result UnpackFileFromSnb(
            [MarshalAs(UnmanagedType.LPStr), In] string snbName,
            [MarshalAs(UnmanagedType.LPStr), In] string relativePath,
            [MarshalAs(UnmanagedType.LPStr), In] string outputName);

        [SuppressMessage("Microsoft.Design", "CA1060")]
        [DllImport("BambookCore.dll",
            EntryPoint = "BambookVerifySnbFile",
            CallingConvention = CallingConvention.Cdecl,
            CharSet = CharSet.Ansi,
            BestFitMapping = false,
            ThrowOnUnmappableChar = true,
            ExactSpelling = true,
            PreserveSig = true)]
        internal static extern Result VerifySnbFile(
            [MarshalAs(UnmanagedType.LPStr), In] string snbName);

        [SuppressMessage("Microsoft.Design", "CA1060")]
        [DllImport("BambookCore.dll",
            EntryPoint = "BambookGetErrorString",
            CallingConvention = CallingConvention.Cdecl,
            CharSet = CharSet.Ansi,
            BestFitMapping = false,
            ThrowOnUnmappableChar = true,
            ExactSpelling = true,
            PreserveSig = true)]
        internal static extern string GetErrorString(
            [MarshalAs(UnmanagedType.I4), In] Result nCode);
    }
}
